package me.zzp.domain;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;

import java.lang.reflect.Field;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

/**
 * @author zhangzepeng
 */
@Slf4j
@Data
public class Page<T> implements Iterable<T> {

    private static class FieldComparator<T> implements Comparator<T> {

        private final List<Order> sort;

        public FieldComparator(List<Order> sort) {
            this.sort = sort;
        }

        @Override
        public int compare(T a, T b) {
            Class<? extends Object> type = a.getClass();

            int result = 0;
            for (Order order : sort) {
                try {
                    Field field = type.getDeclaredField(order.getProperty());

                    field.setAccessible(true);
                    Object valueA = field.get(a);
                    Object valueB = field.get(b);

                    if ((valueA == null && valueB == null) || !(valueA instanceof Comparable) || !(valueB instanceof Comparable)) {
                        continue;
                    }

                    if (valueA != null && valueB != null) {
                        Comparable comparableA = (Comparable) valueA;
                        Comparable comparableB = (Comparable) valueB;
                        result = comparableA.compareTo(comparableB);
                    } else if (valueA != null && valueB == null) {
                        result = 1;
                    } else if (valueA == null && valueB != null) {
                        result = -1;
                    }
                } catch (NoSuchFieldException e) {
                    log.warn("no such field({}) of type({})", order.getProperty(), type.getName());
                } catch (IllegalAccessException e) {
                    log.warn("read field({}) of type({}) failed", order.getProperty(), type.getName());
                }

                if (result != 0) {
                    if (order.getDirection() == Order.Direction.desc) {
                        result = -result;
                    }
                    break;
                }
            }

            return result;
        }
    }

    private static <T> List<T> slice(List<T> list, int page, int size) {
        int offset = page * size;
        if (offset >= list.size()) {
            return Collections.EMPTY_LIST;
        } else if (offset + size > list.size()) {
            return list.subList(offset, list.size());
        } else {
            return list.subList(offset, offset + size);
        }
    }

    private int totalElements;
    private int page;
    private int size;
    private List<T> content;
    private Map<String, Object> attributes = new LinkedHashMap<>();

    public Page() {
        this(new LinkedList<>());
    }

    public Page(List<T> content) {
        this(content, 0, content.size());
    }

    public Page(List<T> content, Pageable pageable) {
        if (pageable.getSort() != null && !pageable.getSort().isEmpty()) {
            content.sort(new FieldComparator<>(pageable.getSort()));
        }
        totalElements = content.size();
        page = pageable.getPage();
        size = pageable.getSize();
        this.content = slice(content, page, size);
    }

    public Page(List<T> content, int page, int size) {
        this(slice(content, page, size), page, size, content.size());
    }

    public Page(List<T> content, int page, int size, int totalElements) {
        this.content = content;
        this.page = page;
        this.size = size;
        this.totalElements = totalElements;
    }

    public int getTotalPages() {
        if (getSize() == 0) {
            return 0;
        }
        return getTotalElements() / getSize() + (getTotalElements() % getSize() == 0? 0: 1);
    }

    public int getNumberOfElements() {
        return content.size();
    }

    public boolean isFirst() {
        return getPage() <= 0;
    }

    public boolean isLast() {
        return getPage() + 1 >= getTotalPages();
    }

    public boolean contains(String name) {
        return attributes != null && attributes.containsKey(name);
    }

    public <T> T get(String name) {
        return (T) (contains(name)? attributes.get(name): null);
    }

    public void put(String name, Object value) {
        attributes.put(name, value);
    }

    @Override
    public Iterator<T> iterator() {
        return content.iterator();
    }
}
